/**
 * FssServer.java
 */
package fss.remote;

import java.io.File;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import fss.common.communication.DownloadRequest;
import fss.common.communication.PingRequest;
import fss.common.communication.QueryResult;
import fss.common.communication.Request;
import fss.common.communication.UploadRequest;
import fss.common.communication.UploadResponse;
import fss.core.config.FssProperties;
import fss.core.logger.FssLogger;
import fss.remote.util.RemoteClientHeartbeater;
import fss.remote.util.RemoteFileService;
import fss.remote.util.RemoteFileServiceRegistry;
import fss.remote.util.ServiceRegistryHeartbeater;
import fss.transfer.Transfer;
import fss.transfer.TransferId;
import fss.transfer.TransferManager;
import fss.transfer.TransferUtils;
import fss.util.FssConstants;
import fss.util.FssUtils;
import fss.util.UtcDate;
import fss.util.rmi.RmiId;
import fss.util.rmi.RmiUtils;

/**
 * The Remote File Service.
 * 
 * @author thinh
 *
 */
public class FssServer extends Observable implements RemoteService, RemoteFileService {
    private static final long serialVersionUID = 1L;
    /**
     * The data directory (includes the '/').
     */
    private static final String DATA_DIR = "/data";
    /**
     * A list of registered remote object identifiers.
     */
    private Set<RmiId> registration = new HashSet<RmiId>();
    /**
     * Home directory for this service.
     */
    private String homedir = null;
    /**
     * Data directory for this service (i.e. the directory where files are stored).
     */
    private String datadir = null;
    /**
     * The service id.
     */
    private RmiId id = null;
    /**
     * The id of the registry server that this service is registered to.
     */
    private RmiId registryId = null;
    /**
     * The heartbeater thread to ping the service registry.
     */
    private ServiceRegistryHeartbeater serviceHeartbeater = null;
    /**
     * Heartbeats the registered remote client(s).
     */
    private RemoteClientHeartbeater clientHeartbeater = null;
    /**
     * The transfer manager.
     */
    private TransferManager transferManager = null;
    /**
     * Start flag.
     */
    private boolean started = false;
    /**
     * The flag indicate whether or not to auto resume all incomplete
     * transfer(s) when the service starts.
     */
    private boolean autoresume = false;
    /**
     * The client poll time.
     */
    private int clientPollTime = -1;
    /**
     * Constructor which allows the caller to set the autoresume flag to 
     * have the service resume incomple transfer(s) if some exists.
     * @param id the id.
     * @param registryId the identifier to the service registry.
     * @param autoresume the auto resume flag.
     * @throws RemoteException error in remote invocation.
     */
    public FssServer(RmiId id, RmiId registryId, boolean autoresume) {
        this.id = id;
        this.registryId = registryId;
        this.autoresume = autoresume;
        homedir = FssProperties.getInstance().getRemoteServiceHomeDir(id.getName());
        datadir = homedir + DATA_DIR;
        FssUtils.createDirectory(homedir);
        FssUtils.createDirectory(datadir);
        clientPollTime = FssProperties.getInstance().getRemoteClientPollTime();
        transferManager = new TransferManager(datadir);
        FssLogger.getInstance().getRemoteServiceLogger(id.getName()).info("FssServer " + id + " successfully instantiated.");
    }
    /**
     * Constructor creating a remote service with specified id. 
     * When the service starts, it will <b>not</b> auto resume incomplete 
     * tranfer(s) if one or more exists.
     * @param id the id.
     * @param registryId the identifier to the service registry.
     * @throws RemoteException error in remote invocation.
     */
    public FssServer(RmiId id, RmiId registryId) {
        this(id, registryId, false);
    }
    /**
     * Get the home directory for this file server.
     * @return the homedir.
     */
    public String getHomedir() {
        return homedir;
    }
    /**
     * Get the data directory for this file server.
     * @return the datadir.
     */
    public String getDatadir() {
        return datadir;
    }
    @Override
    public RmiId getId() throws RemoteException {
        return id;
    }
    @Override
    public void setRegistryId(RmiId id) throws RemoteException {
        this.registryId = id;
    }
    @Override
    public RmiId getRegistryId() throws RemoteException {
        return registryId;
    }
    @Override
    public void update(Object observable, Object message) throws RemoteException {
        Logger logger = FssLogger.getInstance().getRemoteServiceLogger(id.getName());
        /*
         * Whenever a ping request comes, this service (Remote File Service)
         * will build all the list of file(s) it has and call them back
         * to the Remote File Service Registry to update its cache.
         */
        if(message instanceof PingRequest) {
            if(observable instanceof RemoteFileServiceRegistry) {
                RemoteFileServiceRegistry registry = (RemoteFileServiceRegistry) observable;
                List<QueryResult> files = getFileList();
                for(QueryResult f : files) {
                    QueryResult qr = new QueryResult(id, f.getFilename(), f.getFilesize());
                    registry.update(qr);
                }
            }
        }
        /*
         * Whenever a Transfer a File Service expects is an upload
         */
        else if(message instanceof Transfer) {
            Transfer transfer = (Transfer) message;
            TransferUtils.handleTransfer(logger, datadir, transfer);
        }
        /*
         * OTHER MESSAGES CAN GO NEXT
         */
    }
    @Override
    public void stop() throws RemoteException {
        if(serviceHeartbeater != null) {
            Thread _heartbeater = serviceHeartbeater;
            serviceHeartbeater = null;
            _heartbeater.interrupt();
        }
        if(clientHeartbeater != null) {
            Thread _heartbeater = clientHeartbeater;
            clientHeartbeater = null;
            _heartbeater.interrupt();
        }
    }
    @Override
    public void register(RemoteClient client) throws RemoteException {
        RemoteClientObserver ob = new RemoteClientObserver(client);
        boolean added = registerLocal(client);
        // only register observer if it was successfully added to the local registration
        if(added) {
            super.addObserver(ob);
            FssLogger.getInstance().getRemoteServiceLogger(id.getName()).info("Registered client " + client.getId());
        } 
    }
    @Override
    public List<QueryResult> getFileList() throws RemoteException {
        List<QueryResult> contents = getLocalFileList();
        return contents;
    }
    @Override
    public void deleteObserver(Observer observer) {
        if(observer instanceof RemoteClientObserver) {
            RemoteClientObserver o = (RemoteClientObserver) observer;
            try {
                boolean removed = unregister(o.getId());
                FssLogger.getInstance().getServiceRegistryLogger(id.getName()).info("Unregistered " + o.getId() + "(" + removed + ")");
            } catch (RemoteException e) {
                FssLogger.getInstance().getRemoteServiceLogger(id.getName()).warning("Error unregistering remote service: " + e.toString());
            }
        }
        super.deleteObserver(observer);
    }
    @Override
    public void start() throws RemoteException {
        if(!started) {
            Logger logger = FssLogger.getInstance().getRemoteServiceLogger(id.getName());
            
            if(serviceHeartbeater == null) {
                serviceHeartbeater = new ServiceRegistryHeartbeater(id, registryId);
                serviceHeartbeater.start();
            }
            if(clientHeartbeater == null) {
                clientHeartbeater = new RemoteClientHeartbeater(id, clientPollTime);
                clientHeartbeater.start();
            }
            StringBuilder m = new StringBuilder();
            m.append("\n********************************************************************************\n");
            m.append(id + " Started at " + new UtcDate() + "\n");
            m.append("********************************************************************************\n");
            logger.info(m.toString());
            
            /*
             * if autoresume is set, process incomplete transfer(s)
             */
            if(autoresume) {
                List<Transfer> incomplete = null;
                try {
                    incomplete = TransferUtils.getIncompleteTransferList(datadir);
                    for(Transfer transfer : incomplete) {
                        TransferId transferId = transfer.getId();
                        // call the resume 
                        resume(transferId);
                    }
                } catch(Exception e) {
                    logger.severe("Cannot auto resume incomplete transfer(s) on startup. Clearing incomplete data: " + e);
                    TransferUtils.removeAllIncompleteTransfers(datadir);
                    autoresume = false;
                }
            }            
            started = true;
        }
    }
    @Override
    public List<QueryResult> query(String param) throws RemoteException {
        FssLogger.getInstance().getRemoteServiceLogger(id.getName()).info("" + new UtcDate() + " - Invoked on " + id);
        List<QueryResult> matches = new ArrayList<QueryResult>();
        String regex = FssConstants.REGEX_ANY_STRING + param + FssConstants.REGEX_ANY_STRING;
        Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        List<QueryResult> contents = getLocalFileList();
        for(QueryResult file : contents) {
            Matcher matcher = pattern.matcher(file.getFilename());
            if(matcher.matches()) {
                matches.add(file);
            }
        }
        return matches;
    }
    @Override
    public Map<TransferId, Transfer> getStatus() throws RemoteException {
        return transferManager.getTransfers();
    }
    @Override
    public Set<RmiId> getRegisteredClientList() throws RemoteException {
        return registration;
    }
    @Override
    public void processRequest(Request request) throws RemoteException {
        Logger logger = FssLogger.getInstance().getRemoteServiceLogger(id.getName());
        if(request == null) {
            FssLogger.getInstance().getRemoteServiceLogger(id.getName()).info("Received null request. Not processing.");
            return;
        }
        /*
         * Download Request received - delegate to the transfer manager.
         */
        if(request instanceof DownloadRequest) {
            logger.info("Received Download Request from " + request.getSource());
            transferManager.process(request);
        } 
        /*
         * Upload Request received.
         * !!! ASSUMPTION !!! The client requesting to do an upload is registered with this
         * 
         * Upload procedures from the server perspective:
         * 1. Create a response indicating that the File Service is ready for the upload transfer.
         * 2. Broadcast that response to all its clients (but ONLY the one registered to it will process it).
         * 3. Lastly, the first service will just listen for transfer(s).
         */
        else if(request instanceof UploadRequest) {
            UploadRequest upload = (UploadRequest) request;
            TransferId transferId = upload.getTransferId();
            Transfer current = transferManager.getTransfer(transferId);
            
            // the file service is currently processing/waiting for this upload transfer
            if(current != null) {
                logger.warning("Recevied an Upload Request for an upload transfer currently being processed: " + current);
            } else {
                UploadResponse response = new UploadResponse(upload.getSource(), upload.getTransferId());
                broadcastUpload(response);
            }
        }
        
    }
    @Override
    public int getDownloadCount() throws RemoteException {
        return transferManager.getDownloadCount();
    }
    @Override
    public int getUploadCount() throws RemoteException {
        return transferManager.getUploadCount();
    }
    @Override
    public List<Transfer> getIncompleteTransferList() throws RemoteException {
        List<Transfer> incomplete = new ArrayList<Transfer>();
        try {
            incomplete = TransferUtils.getIncompleteTransferList(datadir);
            return incomplete;
        } catch(Exception e) {
            throw new RemoteException("Error obtaining the incomplete transfer list from " + id, e);
        }
    }
    @Override
    public void resume(TransferId transferId) throws RemoteException {
        Logger logger = FssLogger.getInstance().getRemoteServiceLogger(id.getName());

        boolean found = false;
        List<Transfer> incomplete = TransferUtils.getIncompleteTransferList(datadir);
        for(Transfer transfer : incomplete) {
            if(transfer.getId().equals(transferId)) {
                logger.info("Incomplete Transfer found: " + transfer);
                UploadResponse response = new UploadResponse(id, transferId);
                response.setOffset(transfer.getOffset()); // set the offset
                // atempt to first register the client (the source for upload)
                RemoteClient client = (RemoteClient)RmiUtils.getRemoteObject(transfer.getId().getSourceId());
                register(client);
                broadcastUpload(response);
                found = true;
                break; // no need to continue looking through incomplete transfer list
            }
        }
        if(!found) {
            throw new RemoteException("Cannot find incomplete transfer for Transfer ID: " + transferId);
        }
    }
    /**
     * Get a list of all file(s) on the file service.
     * @return a list of file(s) or an empty list.
     * @throws RemoteException error getting the local file list.
     */
    private List<QueryResult> getLocalFileList() throws RemoteException {
        List<QueryResult> result = new ArrayList<QueryResult>();
        File dir = new File(datadir);
        File[] contents = dir.listFiles();
        for(File file : contents) {
            QueryResult qr = new QueryResult(id, file.getName(), file.length());
            qr.setDownloadCount(getDownloadCount());
            qr.setUploadCount(getUploadCount());
            result.add(qr);
        }
        return result;
    }
    /**
     * Register the remote client to the internal registration.
     * @param client the service.
     * @return true if registeration was completed; false otherwise.
     * @throws RemoteException error with registration.
     */
    private synchronized boolean registerLocal(RemoteClient client) throws RemoteException {
        boolean added = false;
        try {
            // only add the registration and the observer if it's not already registered
            RmiId serviceId = client.getId();
            added = registration.add(serviceId);
            return added;
        } catch (RemoteException e) {
            FssLogger.getInstance().getServiceRegistryLogger(id.getName()).warning("Error registering remote client: " + e.toString());
            // do not add if something fails
            throw e;
        }
    }
    /**
     * The internal logic to unregister a remote service. 
     * <p>
     * This is used to remove observer objects.
     * @param id the remote client id.
     * @throws RemoteException error removing the remote service.
     */
    private synchronized boolean unregister(RmiId id) throws RemoteException {
        boolean removed = true;
        if(registration.contains(id)) {
            removed = registration.remove(id);
        }
        return removed;
    }
    /**
     * Broadcast an upload response to all the registered remote client(s).
     * Only the intended remote client will process the Upload Response.
     * @param response the upload response.
     */
    private void broadcastUpload(UploadResponse response) {
        setChanged();
        notifyObservers(response);
    }
}
